/* Controller.m
 * Part of the Moon application for the NeXT computer.
 * Author:  Geoffrey S. Knauth
 * Date:    January 4, 1992
 *
 * Permission to copy this program is hereby granted under the terms
 * of the Free Software Foundation's GNU General Public License.
 */

/* Initially generated by Interface Builder */

#import <strings.h>			/* strcpy */
#import <time.h>			/* struct tm, gmtime, etc. */
#import <dpsclient/dpsclient.h>		/* DPSAddTimedEntry */
#import <appkit/Application.h>		/* NX_BASETHRESHOLD */
#import <appkit/Font.h>			/* NX_IDENTITYMATRIX */
#import <appkit/publicWraps.h>		/* NXBeep */
#import <objc/NXStringTable.h>
#import "all.h"
#import "Controller.h"
#import "MoonView.h"
#import "MoonIconView.h"

static DPSTimedEntry te = 0;

void tick(DPSTimedEntry teNumber, double now, void *userData);

@implementation Controller

- appDidHide :sender
{
    [iconView display];
    return self;
}

- appDidInit:sender
{
    NXRect iconRect = {{0.0, 0.0}, {64.0, 64.0}};
    long t;			/* seconds since 0000 UTC 1/1/1970 */
    struct tm *tmLcl;
    char buf[80];

    nextNewMoon = 0.0;		/* invalidate new moon info */

  /* make our icon view be the appIcon window's contentView */
    iconView = [[MoonIconView alloc] initFrame:&iconRect];
    [[[NXApp appIcon] setContentView:iconView] free];
    
  /* The nowButton should not be enabled until the user has gone into
   * time travel mode.
   */
    [nowButton setEnabled:NO];

    [statForm setTextFont:
     [Font newFont:"Ohlfs" size:10.0 matrix:NX_FLIPPEDMATRIX]];

  /* The local time title must be big enough to hold "Local (GMT+11:30)". */
    (void) time(&t);
    tmLcl = localtime(&t);
    sprintf(buf, "Local (%s)", tmLcl->tm_zone);
    [statForm setTitle:buf at:iLocalTime];

  /* Make sure the function tick (which sends us an update message)
   * is called on a regular basis.
   */
    te = DPSAddTimedEntry(TICK_SECONDS, tick, self, NX_BASETHRESHOLD);

    [self update];	/* update now, in case the wait is long */

    return self;
}

- appWillTerminate :sender
{
    DPSRemoveTimedEntry(te);
    return self;
}

- getUserGmtTime :(int *)year :(int *)month :(int *)day
		 :(int *)hour :(int *)minute :(int *)second
{
    char buf[80], monthStr[20];
    int i, n, expected, hhmm;

  /* Here are the kinds of input we can accept:
   * hh:mm:ss dd mmmmm yyyy	%d:%d:%d %d %s %d	(6)
   * hh:mm dd mmmmm yyyy	%d:%d %d %s %d		(5)
   * hhmm dd mmmmm yyyy		%d %d %s %d		(4)
   * dd mmmmm yyyy		%d %s %d		(3)
   * mmmmm yyyy			%s %d			(2)
   * yyyy			%d			(1)
   */
    strcpy(buf, [travelText stringValue]);
    for (i = n = 0; i < 6; n = 0, i++) {
	expected = *hour = *minute = *second = *day = *month = *year = 0;
	switch (i) {
	  case 0:
	    expected = 6;
	    n = sscanf(buf, "%d:%d:%d %d %s %d",
		       hour, minute, second, day, monthStr, year);
	    break;
	  case 1:
	    expected = 5;
	    n = sscanf(buf, "%d:%d %d %s %d",
		       hour, minute, day, monthStr, year);
	    break;
	  case 2:
	    expected = 4;
	    n = sscanf(buf, "%d %d %s %d", &hhmm, day, monthStr, year);
	    break;
	  case 3:
	    expected = 3;
	    n = sscanf(buf, "%d %s %d", day, monthStr, year);
	    break;
	  case 4:
	    expected = 2;
	    n = sscanf(buf, "%s %d", monthStr, year);
	    *day = 1;
	    break;
	  case 5:
	    expected = 1;
	    n = sscanf(buf, "%d", year);
	    *day = 1;
	    strcpy(monthStr, [stringTable valueForStringKey:"1"]);
	    break;
	}
	if (n && n == expected) break;
    }
    if (n) {
	*month = [self monthNumFromStr:monthStr];
	if (*month == 0) goto bad;
	if (expected == 4) {
	    *hour   = hhmm / 100;
	    *minute = hhmm % 100;
	}
	return self;
    }
bad:
    *hour = *minute = *second = *day = *month = *year = 0;
    return nil;
}

- (int)monthNumFromStr :(const char *)month
{
    char monthBuf[2+1];
    int i;

    for (i = 12; i > 0; i--) {
	sprintf(monthBuf, "%d", i);
	if (strcmp(month, [stringTable valueForStringKey:monthBuf]) == 0)
	    break;
    }
    return i;
}

- pause :sender
{
  /* We need a pause method, so that when we are coming out of pause,
   * resuming to run, we can generate an update right away, without
   * making the user wait for the next official tick.
   */
    if ([sender state] == RUNNING)	/* state changed before action sent */
	[self update];

    return self;
}

- returnToNow :sender
{
  /* When the nowButton is disabled, we are not in time travel mode.
   * When are not in time travel mode, update gets its time from the system,
   * not from the user.
   */
    [sender setEnabled:NO];		/* sender is also nowButton */
    [pauseButton setEnabled:YES];	/* by definition we will run */
    nextNewMoon = 0.0;			/* invalidate new moon info */
    [self update];
    return self;
}

- timeTravel :sender
{
  /* When the nowButton is enabled, we are in time travel mode.
   * When are in time travel mode, update gets its time from the user,
   * not from the system.
   */
    [nowButton setEnabled:YES];
    [pauseButton setEnabled:NO];	/* by definition we will pause */
    nextNewMoon = 0.0;			/* invalidate new moon info */
    [self update];
    return self;
}

- update
{
    long t;				/* seconds since 0000 UTC 1/1/1970 */
    static long gmtoff = -1L;		/* timezone offset from UTC in secs */
    struct tm *tmUtc, *tmLcl;		/* Unix UTC & local time structures */
    float p;				/* moon phase: 0=new, 0.5=full */
    int lunation;
    double jd, aom, cphase, cdist, cangdia, csund, csuang, lptime;
    double phasar[5];
    double fakeJd;			/* don't use for astro calculations! */
    char tbuf[80];
    char monthBuf[2+1];			/* "1" .. "12" */
    int yy, mm, dd, hh, mmm, ss;	/* moon times */
    int year, month, day;		/* our UTC times */
    int hour, minute, second;		/* our UTC times */
    int lyear, lmonth, lday;		/* our local times */
    int lhour, lminute, lsecond;	/* our local times */

    if ([pauseButton state] == PAUSED) return nil;

    if ([nowButton isEnabled]) {
      /* We're traveling in time.
       * This is not the initial state, so gmtoff is legitimate.
       */
	if ([self getUserGmtTime
	     :&year :&month :&day :&hour :&minute :&second] == nil)
	{
	    NXBeep();
	    [nowButton setEnabled:NO];	/* no time travel after all */
	    return nil;
	}
	jd = ymdhmsToJtime(year, month, day, hour, minute, second);
	fakeJd = jd + (double)gmtoff / 86400.;
    } else {
      /* We're not traveling in time.  Unix gives us our time info.
       * This is the initial state.
       */
	(void) time(&t);
	tmUtc = gmtime(&t);
	jd = jtime(tmUtc);
	year   = tmUtc->tm_year + 1900;
	month  = tmUtc->tm_mon + 1;
	day    = tmUtc->tm_mday;
	hour   = tmUtc->tm_hour;
	minute = tmUtc->tm_min;
	second = tmUtc->tm_sec;
	tmLcl = localtime(&t);
	fakeJd = jtime(tmLcl);
	if (gmtoff == -1L) {
	  /* Save the time zone offset for later.  We'll need it if the user
	   * does any time traveling, because if we go too far into the past
	   * or future, Unix time functions won't help us any more.
	   */
	    gmtoff = tmLcl->tm_gmtoff;	/* remember offset is in seconds */
	}
    }
    p = (float)jdtophase(jd, &cphase, &aom, &cdist, &cangdia, &csund, &csuang);

  /* Draw the big image of the moon and shadow it. */
    [moonView setPhase:p];
    [moonView display];

  /* Draw the little icon image of the moon and shadow it. */
    [iconView setPhase:p];
    [iconView display];

  /* Update textual information */
    
    sprintf(tbuf, "%.5f", jd);	/* 1979 January 1.0 <==> JD 2443874.5 */
    [statForm setStringValue:tbuf at:iJulianDate];

    sprintf(monthBuf, "%d", month);
    sprintf(tbuf, "%02d:%02d:%02d %d %s %d",
	    hour, minute, second, day,
	    [stringTable valueForStringKey:monthBuf], year);
    [statForm setStringValue:tbuf at:iUniversalTime];

    jyear(fakeJd, &lyear, &lmonth, &lday);
    jhms(fakeJd, &lhour, &lminute, &lsecond);
    sprintf(monthBuf, "%d", lmonth);
    sprintf(tbuf, "%02d:%02d:%02d %d %s %d",
	    lhour, lminute, lsecond, lday,
	    [stringTable valueForStringKey:monthBuf], lyear);
    [statForm setStringValue:tbuf at:iLocalTime];

    sprintf(tbuf, "%d%% %s, %s",
	    (int) rint(cphase * 100),
	    [stringTable valueForStringKey:"visible"],
	    p < 0.5 ?
	    [stringTable valueForStringKey:"waxing"]
	    : [stringTable valueForStringKey:"waning"]);
    [statForm setStringValue:tbuf at:iMoonPhase];

  /* Some information about the Moon */

    sprintf(tbuf, "%dd %dh %dm",
	    (int) aom, (int) (24 * (aom - floor(aom))),
	    ((int) (1440 * (aom - floor(aom)))) % 60);
    [statForm setStringValue:tbuf at:iAgeOfMoon];

    sprintf(tbuf, "%ld km, %.1f %s",
	    (long) cdist, cdist / earthrad,
	    [stringTable valueForStringKey:"earthRadii"]);
    [statForm setStringValue:tbuf at:iMoonDistance];

    sprintf(tbuf, "%.4f %s", cangdia,
	    [stringTable valueForStringKey:"degrees"]);
    [statForm setStringValue:tbuf at:iMoonSubtends];

  /* Information about the Sun */
    
    sprintf(tbuf, "%.0f km, %.3f AU", csund, csund / sunsmax);
    [statForm setStringValue:tbuf at:iSunDistance];

    sprintf(tbuf, "%.4f %s", csuang,
	    [stringTable valueForStringKey:"degrees"]);
    [statForm setStringValue:tbuf at:iSunSubtends];

  /* Calculate times of phases of this lunation.  This is sufficiently
   * time-consuming that we only do it once a month, or when we begin or
   * return from time travel.  If we've just changed the time travel mode,
   * nextNewMoon will be zero.
   */
    if (jd > nextNewMoon) {
	phasehunt(jd, phasar);
	lptime = phasar[0];
	lunation = floor(((lptime + 7) - lunatbase) / synmonth) + 1;
	jyear(lptime, &yy, &mm, &dd);
	jhms(lptime, &hh, &mmm, &ss);
	sprintf(monthBuf, "%d", mm);
	sprintf(tbuf, "%02d:%02d UTC %d %s %d", hh, mmm, dd,
		[stringTable valueForStringKey:monthBuf], yy);
	[statForm setStringValue:tbuf at:iLastNewMoon];

	sprintf(tbuf, "%d", lunation);
	[statForm setStringValue:tbuf at:iThisLunation];

	lptime = phasar[1];
	jyear(lptime, &yy, &mm, &dd);
	jhms(lptime, &hh, &mmm, &ss);
	sprintf(monthBuf, "%d", mm);
	sprintf(tbuf, "%02d:%02d UTC %d %s %d", hh, mmm, dd,
		[stringTable valueForStringKey:monthBuf], yy);
	[statForm setStringValue:tbuf at:iFirstQuarter];

	lptime = phasar[2];
	jyear(lptime, &yy, &mm, &dd);
	jhms(lptime, &hh, &mmm, &ss);
	sprintf(monthBuf, "%d", mm);
	sprintf(tbuf, "%02d:%02d UTC %d %s %d", hh, mmm, dd,
		[stringTable valueForStringKey:monthBuf], yy);
	[statForm setStringValue:tbuf at:iFullMoon];

	lptime = phasar[3];
	jyear(lptime, &yy, &mm, &dd);
	jhms(lptime, &hh, &mmm, &ss);
	sprintf(monthBuf, "%d", mm);
	sprintf(tbuf, "%02d:%02d UTC %d %s %d", hh, mmm, dd,
		[stringTable valueForStringKey:monthBuf], yy);
	[statForm setStringValue:tbuf at:iLastQuarter];

	nextNewMoon = phasar[4];
	jyear(nextNewMoon, &yy, &mm, &dd);
	jhms(nextNewMoon, &hh, &mmm, &ss);
	sprintf(monthBuf, "%d", mm);
	sprintf(tbuf, "%02d:%02d UTC %d %s %d", hh, mmm, dd,
		[stringTable valueForStringKey:monthBuf], yy);
	[statForm setStringValue:tbuf at:iNextNewMoon];

	sprintf(tbuf, "%d", lunation + 1);
	[statForm setStringValue:tbuf at:iNextLunation];
    }
    return self;
}

void tick(DPSTimedEntry teNumber, double now, void *userData)
{
    Controller *self = userData;	/* this trick lets C do some Obj-C */

  /* We only came here because of the interface from the DPSTimedEntry.
   * Now let's be more objective.
   */
    if ([self->nowButton isEnabled]) {
      /* We're in time travel mode.
       * That means only the user should be able to generate update events.
       * While in this mode, ticks will be ignored.
       * We do this so that the user may finish typing a time travel date
       * and tell us when to read it, otherwise we are likely to read it
       * prematurely.
       */
	return;
    }
    [self update];
}

@end
